﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Text.RegularExpressions;
using System.Runtime.InteropServices;
using System.Threading;
using ThreadState = System.Threading.ThreadState;

namespace Piao
{
    public delegate string InputCaptchaDelegate(Image img);

    class UserOpration
    {
        [DllImport("winmm")]
        public static extern bool PlaySound(string szSound, IntPtr hMod, int i);

        protected UserOpration()
        { }

        private static UserOpration instance;
        private WebUtil web = new WebUtil();
        private bool _isFirstLogin = true;
        private Image loginImage;
        private bool _isFirstSubmit = true;
        private Image submitImage;
        private string webprompt;
        private string ccc;
        private string orderPost;

        public string ProxyName
        {
            set
            {
                if (!string.IsNullOrEmpty(value))
                {
                    web.proxy = new System.Net.WebProxy(value);
                    Log.Message("change proxy to [{0}]", value.ToUpper());
                }
            }
        }


        public static UserOpration Instance
        {
            get
            {
                if (null == instance)
                    instance = new UserOpration();
                return instance;
            }
        }

        /// <summary>
        /// use a new instance.
        /// </summary>
        /// <returns></returns>
        public static UserOpration NewInstance()
        {
            instance = new UserOpration();
            return instance;
        }

        public string Login(Func<Image, string> inputCaptchaThread, string user, string password)
        {
            Random rnd = new Random(System.DateTime.Now.Millisecond);
            if (_isFirstLogin)
            {
                loginImage = web.DownloadCaptcha(URL_lOGIN_CAPTCHA + "&0." + rnd.Next(10000, 100000));
                _isFirstLogin = false;
                webprompt = string.Empty;

                ccc = inputCaptchaThread(loginImage);
            }


            if (string.IsNullOrEmpty(ccc))
            {
                Log.Error("验证码输入超时或没有有效的验证码。");
                return string.Empty;
            }

            Log.Message("登陆网页中，账号{0}, 验证码 {1}", user, ccc);


            RequestStringBuilder rsb = new RequestStringBuilder();
            rsb["loginUser.user_name"] = user;
            rsb["user.password"] = password;
            rsb["randCode"] = ccc;

            string postData = rsb.ToString();

            string postResponse = web.Post(URL_POST_LOGIN, postData);
            string popup = GetPopupMessage(postResponse);
            try
            {
                Match match = Regex.Match(postResponse, @"系统维护中.*?(?=</)");

                if (match.Success)
                {
                    Log.Error("登陆不成功, {0}", match.Value);
                    return string.Empty;
                }

                match = Regex.Match(postResponse, @"(?<=<h1 .*>).*欢迎您！(?=</h1>)");
                if (!match.Success)
                {
                    Log.Error("登陆不成功, 检查验证码和密码。 {0}", popup);
                    return string.Empty;
                }
                else
                {
                    webprompt = match.Value;
                    Log.Message(webprompt);
                    //string loginpageStr = web.Get(URL_HOME);
                }
            }
            catch (Exception ex)
            {
                Log.Error(ex.ToString());
            }

            return webprompt;

        }

        /// <summary>
        /// from home page to read seat types
        /// </summary>
        public void QuerySeatTypes()
        {
            string response = web.Get(URL_HOME_SEATTYPES);
            this.ReadTicketInfo(response);

            Log.Message("获取车票类型{0}个。 ", Global.TicketCodeList.Count);
        }


        public void QueryStationName()
        {
            string str = web.Get(@URL_STATION_NAMES_JS);
            var matches = Regex.Matches(str, @"\d{0,3}@\D{3}\|.*?\|[a-zA-Z]{3}", RegexOptions.IgnoreCase | RegexOptions.Compiled);

            Log.Message("获取到车站名称{0}个。", matches.Count);

            foreach (Match m in matches)
            {
                string[] arr = m.Value.Split('|');
                string key = arr[2]; //key is code, arr[1] is chinese name text
                if (!Global.StationNameDict.ContainsKey(key))
                {
                    Global.StationNameDict.Add(key, arr[1]);

                    string pj = ChinesePinYin.PinYinInitialLetter.Read(arr[1]);
                    var row = Global.StationNameTable.NewRow();
                    Global.StationNameTable.Rows.Add(row);
                    row[Global.DataComlumnChineseName] = arr[1];
                    row[Global.DataComlumnCodeName] = key;
                    row[Global.DataComlumnPinYin] = pj;
                    row[Global.DataComlumnDisplayText] = key.Substring(0, 1) + "," + arr[1];

                }
            }
        }


        public List<JsonObject> QueryStStrainAll(string date, string fromCode, string toCode)
        {
            if (fromCode == toCode) return new List<JsonObject>();


            RequestStringBuilder rsb = new RequestStringBuilder();
            rsb["date"] = date;
            rsb["fromstation"] = fromCode;
            rsb["tostation"] = toCode;
            rsb["starttime"] = "00%3A00--24%3A00";

            string postData = rsb.BuildString();

            string response = web.Post(URL_POST_QUERY_AVAILABLE_TRAINS, postData);



            if (string.IsNullOrEmpty(response)
                || response.Contains("系统忙") ||
                response.Contains("系统维护中")
                ) return new List<JsonObject>();

            using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(response)))
            {
                DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(List<JsonObject>));
                List<JsonObject> list = (List<JsonObject>)serializer.ReadObject(ms);


                foreach (JsonObject t in list)
                {
                    if (!Global.TrainCodeList.Contains(t))
                    {
                        Global.TrainCodeList.Add(t);
                    }
                }


                Log.Message("从 {0} 到 {1} 有 {2} 个车次.", Global.StationNameDict[fromCode], Global.StationNameDict[toCode], list.Count);
                return list;
            }
        }


        public bool QueryLeftTicket(string date, ref string fromCode, ref string toCode, string trainCode, ref OrderData odata)
        {
            /*
             * temp fix, in this function, all status of global trains get updated, this is for listbox for display start-end time of select train
             * whatever values changes in current train in global, should be reflected to current order
             * under this circumstance have to add an ref parameter at end.
             */


            if (Global.StationNameDict.Count == 0)
                QueryStationName();

            JsonObject train = Global.TrainCodeList.Find(t => t.id == trainCode);

            if (train == null)
                return false;


            RequestStringBuilder rsb = new RequestStringBuilder();
            Random rd = new Random(DateTime.Now.Millisecond);

            rsb["orderRequest.train_date"] = date;
            rsb["orderRequest.from_station_telecode"] = fromCode;
            rsb["orderRequest.to_station_telecode"] = toCode;
            rsb["orderRequest.train_no"] = trainCode;
            rsb["trainPassType"] = "QB";
            rsb["trainClass"] = "QB%23D%23Z%23T%23K%23QT%23";
            rsb["includeStudent"] = "00";
            rsb["seatTypeAndNum"] = "";
            rsb["orderRequest.start_time_str"] = "00%3A00--24%3A00";
            rsb["httprandstr"] = rd.Next(10000, 20000).ToString();

            string url = URL_QUERY_LEFT_TICKET + "&" + rsb.BuildString();

            string response = web.Get(url);

            //format is quite like : 
            //onclick=javascript:getSelected('T27#19:41#20:09#2400000T2709#BXP#XXO#15:50#北京西#西宁西#1020800003303670000040584000001020803105')

            Match content = Regex.Match(response, @"(?<=<input .*onclick=.*?)'.*?'", RegexOptions.IgnoreCase);
            var codeMatches = Regex.Matches(content.Value, @"(?<=#)[a-z]{3}", RegexOptions.IgnoreCase);

            if (codeMatches.Count == 2)
            {
                fromCode = codeMatches[0].Value;
                toCode = codeMatches[1].Value;
            }


            train.lishi = Regex.Match(content.Value, @"(?<=#).*?(?=#)", RegexOptions.IgnoreCase).Value;
            train.ypInfoDetail = Regex.Match(content.Value, @"(?<=#)[a-z0-9]+(?='|$)", RegexOptions.IgnoreCase).Value;

            odata.lishi = train.lishi;
            odata.ypInfoDetail = train.ypInfoDetail;


            if (response.Length < 10)
            {
                PlaySound(@"C:\Windows\Media\Windows Critical Stop.wav", IntPtr.Zero, 1);
                Log.Error("登录超时, 需要重新登录。 ");
                return false;
            }

            response = Regex.Match(response, @"(?<=\d{2}:\d{2},\d{2}:\d{2},).*(?=<input)", RegexOptions.IgnoreCase).Value;
            bool hasValidTicket = false;

            #region  test code

            //string order copied from webpage
            string[] nameSplitted = "商务座,特等座,一等座,二等座,高级软卧,软卧,硬卧,软座,硬座,无座,其他".Split(',');
            string[] valueSplitted = new string[nameSplitted.Length];
            try
            {
                int idx = 0;

                foreach (string s in response.Split(','))
                {
                    var matchFull = Regex.Match(s, @"((?<=<font.*)有|(?<=<font.*>)\d+|\d+(?=$))", RegexOptions.IgnoreCase);
                    valueSplitted[idx] = matchFull.Value;

                    if (matchFull.Success)
                    {
                        Log.Message("{0}, 从 {1} 到 {2} 的车次 {3}, 余票状态 {4} {5}。", date + DateTime.Now.ToString(" hh:mm:ss"),
                         Global.StationNameDict[fromCode],
                         Global.StationNameDict[toCode],
                         train.value,
                         nameSplitted[idx], valueSplitted[idx]);
                    }

                    if (nameSplitted[idx] != "无座" && nameSplitted[idx] != "其他")
                        hasValidTicket = hasValidTicket == true ? true : matchFull.Success;

                    idx++;

                    if (idx == nameSplitted.Length)
                        break;
                }
            }
            catch (Exception)
            {
            }
            #endregion

            Match match = Regex.Match(response, @"(?<=<font.*)有|(?<=<font.*>)\d+|\d+(?=,)", RegexOptions.IgnoreCase);
            //<font color='#008800'>有</font>


            bool hasTicket = match.Success && hasValidTicket;

            if (hasTicket)
            {
                PlaySound(@"C:\Windows\Media\tada.wav", IntPtr.Zero, 1);
            }

            Log.Message("{0}, 从 {1} 到 {2} 的车次 {3}, 余票状态 [{4}]。\r\n---------------------------", date + DateTime.Now.ToString(" hh:mm:ss"),
                Global.StationNameDict[fromCode],
                Global.StationNameDict[toCode],
                train.value,
                hasTicket ? match.Value : "无"); // “无票”不计入结果
            return hasTicket;

        }

        /// <summary>
        /// submit order. if return not-null value, represents this is fatal error should stop, otherwise can retry again.
        /// </summary>
        /// <param name="data"></param>
        /// <param name="user"></param>
        /// <param name="inputCaptchaThread"></param>
        /// <returns></returns>
        public string SubmitOrderRequest(OrderData data, UserData user, Func<Image, string> inputCaptchaThread)
        {
            bool isSuccessed = false;
            string ccc = string.Empty;
            while (!isSuccessed)
            {
                System.Net.HttpStatusCode code = 0;
                Dictionary<string, string> headers = new Dictionary<string, string>()
                                                         {
                                                             {"Accept-Encoding", "gzip, deflate"},
                                                             {"Accept-Language", "zh-CN"}
                                                         };


                string postData = data.ToPostString();
                string response = string.Empty;
                const int MAX_TRY_TIME = 5;

                for (int time = 1;;)
                {
                    CheckThreadStateToAbort();
                    response = web.Post(URL_POST_SUBMIT_ORDER, postData, null, headers, out code);

                    if (time > MAX_TRY_TIME)
                    {
                        Log.Error("出现 [系统忙] 错误。 原因：用户提交数据出错， 车次可能有误。");
                        return "系统忙";
                    }
                    else if (response.Contains("系统忙"))
                    {
                        Log.Error("系统忙, 重试{0}次。", time);
                    }
                    else
                        break;

                    time++;
                }

                string popup = GetPopupMessage(response);
                if (popup.Contains("目前您还有未处理的订单"))
                {
                    Log.Error("提交停止， {0}", popup);
                    return "目前您还有未处理的订单.";
                }

                this.ReadTicketInfo(response);
                Log.Message("已经查询到 {0} 余票状态。", data.station_train_code);

                //the value of 'lishi' is assigned in VerifyHasTicket()
                //when '有票' the field will have a value represent the seat code
                JsonObject ticket = null;
                if (!this.VerifyHasTicket(response) ||
                    (ticket = Global.SeatTypeTargets.Find(j => !string.IsNullOrEmpty(j.lishi))) == null)
                {
                    Log.Error("提交失败，选中类型无票。");
                    return "选中类型无票。";
                }
                user.passenger_1_seat = ticket.id;

                var submitDataDict = this.ReadOrderRequestFormData(response);
                var userFields =
                    user.GetType().GetFields(System.Reflection.BindingFlags.Public |
                                             System.Reflection.BindingFlags.Instance);
                foreach (var fi in userFields)
                {
                    string orderFieldVal = fi.GetValue(user) as string;

                    if (submitDataDict.ContainsKey(fi.Name))
                        submitDataDict[fi.Name] = orderFieldVal;
                    else
                        submitDataDict.Add(fi.Name, orderFieldVal);
                }

                submitDataDict["passengerTickets"] = user.passengerTickets();
                submitDataDict["oldPassengers"] = user.oldPassengers();
                submitDataDict.Add("orderRequest.reserve_flag", "A");

                StringBuilder submitText = new StringBuilder();

                foreach (var pair in submitDataDict)
                {
                    submitText.AppendFormat("&{0}={1}", pair.Key, System.Web.HttpUtility.UrlEncode(pair.Value));
                }


                Random random = new Random(DateTime.Now.Millisecond);

                string orderResponse = string.Empty;

                CheckThreadStateToAbort();
                if (_isFirstSubmit)
                {
                    var image = web.DownloadCaptcha(URL_SUBMIT_CAPTCHA);
                    ccc = inputCaptchaThread(image);
                    _isFirstSubmit = false;
                if (String.IsNullOrEmpty(ccc))
                    return "ccc empty";
                }

                string orderPost = string.Format("{0}&randCode={1}&{2}", submitText.ToString().TrimStart('&'), ccc,
                                                 random.Next(10000,20000).ToString());


                orderResponse = web.Post(URL_POST_COMFIRM_TICKET_ORDER, orderPost, URL_POST_COMFIRM_TICKET_ORDER,
                                         headers, out code);


                string postResult = GetPopupMessage(orderResponse);
                if (!string.IsNullOrEmpty(postResult))
                {
                    Log.Error("提交中遇到错误， {0}。 ", postResult);
                    if (postData.Contains("验证码") || postData.Contains("证件号码输入有误"))
                        return postResult;
                }


                if (orderResponse.Contains("成功"))
                {
                    isSuccessed = true;
                    Log.Message("订单已确认, 正在打开登陆页面。");
                }
            }

            return string.Empty;
        }

        public static void CheckThreadStateToAbort()
        {
            //bool reasonFormReCreated = !MainForm.Instance.Visible && Thread.CurrentThread.ThreadState == ThreadState.Running;
            bool reasonThreadAbortSent = Thread.CurrentThread.ThreadState == ThreadState.AbortRequested;
            if (reasonThreadAbortSent)
            {
                //Debug.WriteLine(string.Format("Thread {0} was aborted by reasonFormReCreated {1}, reasonThreadAbortSent {2}",
                //    Thread.CurrentThread.ManagedThreadId,
                //    reasonFormReCreated,
                //    reasonThreadAbortSent));|

                try
                { Thread.CurrentThread.Abort(); }
                catch { }
            }

        }


        private string GetPopupMessage(string htmltext)
        {
            Match err = Regex.Match(htmltext, @"(?<=<script language=""javascript"">.*var message = "").*(?="";.*var messageShow)", RegexOptions.IgnoreCase | RegexOptions.Singleline);
            return err.Value;
        }

        private bool VerifyHasTicket(string text)
        {
            var matches = Regex.Matches(text, @"(?<=<td>).*?(.*元).*(有票|\d+张票)(?=</td>)", RegexOptions.IgnoreCase);

            foreach (Match m in matches)
            {
                if (!m.Success)
                    continue;
                //display all availble tickets and update info at background
                Log.Message(m.Value);

                foreach (var obj in Global.SeatTypeTargets)
                {
                    if (m.Value.Contains(obj.value))
                    {
                        //this value represents the seat code for submit
                        obj.lishi = m.Value;
                        break;
                    }
                }
            }

            return matches.Count > 0;
        }

        private void ReadTicketInfo(string text)
        {
            List<JsonObject> allObjects = new List<JsonObject>();
            var matchCollection = Regex.Matches(text, @"var .*?(\[\{|\{)?.*?(\]\}|\})?;", RegexOptions.IgnoreCase | RegexOptions.Multiline);

            foreach (Match tmp in matchCollection)
            {
                var singleObjectText = Regex.Matches(tmp.Value, @"\[\{.*?\}\]", RegexOptions.IgnoreCase);

                foreach (Match objectMatch in singleObjectText)
                {
                    using (var ms = new MemoryStream(Encoding.UTF8.GetBytes(objectMatch.Value)))
                    {
                        DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(List<JsonObject>));
                        List<JsonObject> list = (List<JsonObject>)serializer.ReadObject(ms);

                        allObjects.AddRange(list);
                    }//deserializer objects
                } // end of handling json objects in parsed lines
            } //end of handling all json in response


            foreach (var jobj in allObjects)
            {
                bool containsKey = false;
                foreach (var savedObj in Global.TicketCodeList)
                {
                    if (jobj.id == savedObj.id && jobj.value == savedObj.value)
                    {
                        containsKey = true;
                        break;
                    }
                }

                if (!containsKey && !string.IsNullOrEmpty(jobj.id))
                {
                    Global.TicketCodeList.Add(jobj);
                }

            }




        }

        private Dictionary<string, string> ReadOrderRequestFormData(string text)
        {
            var matches = Regex.Matches(text, @"<input type=['""]{1}hidden['""]{1}.*?>", RegexOptions.IgnoreCase | RegexOptions.Multiline);

            Dictionary<string, string> inputDict = new Dictionary<string, string>();

            List<string> ignored = new List<string>()
            {
                "textfield","checkbox0","checkbox9", "orderRequest.reserve_flag"
            };

            foreach (Match m in matches)
            {
                string name = Regex.Match(m.Value, @"(?<=name="").*?(?="")", RegexOptions.IgnoreCase).Value;
                string val = Regex.Match(m.Value, @"(?<=value="").*?(?="")", RegexOptions.IgnoreCase).Value;


                if (string.IsNullOrEmpty(name) || inputDict.ContainsKey(name))
                {
                    continue;
                }

                if (!ignored.Contains(name))
                    inputDict.Add(name, val);
            }

            return inputDict;
        }


        public const string URL_HOME = "https://dynamic.12306.cn/otsweb/main.jsp";

        //all webpages use this to generate verification code 
        public const string URL_lOGIN_CAPTCHA = "https://dynamic.12306.cn/otsweb/passCodeAction.do?rand=lrand";
        public const string URL_SUBMIT_CAPTCHA = "https://dynamic.12306.cn/otsweb/passCodeAction.do?rand=randp";

        //login urls
        const string URL_LOGIN_PAGE = @"https://dynamic.12306.cn/otsweb/loginAction.do?method=init";
        const string URL_POST_LOGIN = @"https://dynamic.12306.cn/otsweb/loginAction.do?method=login";

        //train id, seat type info

        const string URL_HOME_SEATTYPES = @"https://dynamic.12306.cn/otsweb/order/querySingleAction.do?method=init";
        const string URL_STATION_NAMES_JS = @"https://dynamic.12306.cn/otsweb/js/common/station_name.js?version=1.3";

        //from locationA to locationB, train info
        const string URL_POST_QUERY_AVAILABLE_TRAINS = @"https://dynamic.12306.cn/otsweb/order/querySingleAction.do?method=queryststrainall";
        const string URL_QUERY_LEFT_TICKET = "https://dynamic.12306.cn/otsweb/order/querySingleAction.do?method=queryLeftTicket";

        //TODO: URL TYPO
        //order request urls
        const string URL_POST_SUBMIT_ORDER = "https://dynamic.12306.cn/otsweb/order/querySingleAction.do?method=submutOrderRequest";
        const string URL_POST_COMFIRM_TICKET_ORDER = "https://dynamic.12306.cn/otsweb/order/confirmPassengerAction.do?method=confirmPassengerInfoSingle";

    }
}
